#pragma once 

//头文件按C语言、C++、自定义的顺序写比较清晰
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <iostream>
#include <string>

#include "ConnectInfo.hpp"
#include "UserManager.hpp"
#include "MessagePool.hpp"
#include "tools.hpp"

#define MAX_ROUND_COUNT 10
#define THREAD_COUNT 1   //创建的线程数量

//保存新的套接字描述符的类，用来传递新连接的套接字给入口函数
class TcpConnect
{
    public:
        TcpConnect()
        {
            new_sock_ = -1;
            server_ = NULL; //this指针
        }

        ~TcpConnect()
        {

        }

        void SetSockFd(int fd) //传入fd，设置new_sock
        {
            new_sock_ = fd;
        }

        void SetServer(void* server) //设置this指针
        {
            server_ = server;
        }

        int GetSockFd() //获得new_sock
        {
            return new_sock_; //new_sock_是private的，需要提供接口访问
        }

        void* GetServer() //获得this指针
        {
            return server_;
        }
    private:
        int new_sock_;

        //保存ChatServer这个类的this指针,确保在tcp的线程入口函数当中可以获取到用户管理模块的实例化指针
        //后面要调用用户管理模块的UserManager类的函数
        void* server_;
};


 /* 创建ChatServer类，这个类主要完成的事情：
  * 创建TCP套接字，创建UDP套接字，
  * 创建线程
  * 串联用户管理模块，TCP接收回来的数据需要与用户管理模块打交道
  * 串联消息池，UDP接收回来的消息要放到消息池，需要消息池提供消息的PUSH和POP接口
  * 
  * 把握一条主线，用户管理模块、消息池等等都是ChatServer类的附属品，
  * 主线就是ChatServer类，完成在类中需要调用的不同模块的不同接口，就差不多了
  **/ 
class ChatServer
{
    //先考虑框架
    //即当前的类需要给调用者提供哪些接口，或者自身需要哪些接口完成哪些事情
    //不能死扣某一个函数的实现，先搭框架
    public:
        ChatServer()
        {
            //登录注册，采用TCP
            //当前构造当中没有创造socket，初始值给个-1(公司常用的方法)
            //构造函数是没有返回值的，如果tcp_sock_ = socket(); socket创建失败了怎么办？
            //如果你if判断，创建失败返回-1，但是构造函数是没有返回值的，没办法告诉创建实例化对象的那个人
            tcp_sock_ = -1; //初始化tcp_sock
            tcp_port_ = TCP_PORT; //初始化端口，端口在ConnectInfo中给了个宏

            user_manager_ = NULL; //初始化用户管理模块的实例化指针

            udp_sock_ = -1;
            udp_port_ = UDP_PORT;

            //初始化线程（生产线程和消费线程）
            memset(con_tid_, '\0', THREAD_COUNT*sizeof(pthread_t));
            memset(pro_tid_, '\0', THREAD_COUNT*sizeof(pthread_t));

            msg_pool_ = NULL; //初始化消息池模块的实例化指针

            udp_msg = NULL; //初始化数据格式模块中udp发消息的实例化指针
        }

        ~ChatServer()
        {
            //new出来的东西记得要delete掉
            if(user_manager_)
            {
                delete user_manager_;
                user_manager_ = NULL;
            }

            if(msg_pool_)
            {
                delete msg_pool_;
                msg_pool_ = NULL; //防止野指针
            }

            if(udp_msg)
            {
                delete udp_msg;
                udp_msg = NULL;
            }
        }


        //初始化变量(服务)的接口，被调用者调用的接口，定义一个对象，调用此函数用来初始化类中的变量
        //包含：用户管理模块的实例化对象，消息池的实例化对象
        int InitSvr(uint16_t tcp_port = TCP_PORT) //缺省参数，没传入就用TCP_PORT,传入就用传入的
        {
            //1.创建tcp-socket，并且绑定地址信息，监听
            tcp_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            if(tcp_sock_ < 0) //创建套接字失败
            {
                return -1;
            }

            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(tcp_port);
            addr.sin_addr.s_addr = inet_addr("0.0.0.0");//0.0.0.0表示侦听当前机器当中的任意网卡信息
            int ret = bind(tcp_sock_, (struct sockaddr*)&addr, sizeof(addr));
            //inet_addr：1.将点分十进制的IP地址转换成为uint32_t
            //           2.将uint32_t从主机字节序转换成为网络字节序
            //addr是个对象可以sizeof(addr),也可以sizeof(struct sockaddr_in)，计算addr的字节数量，但是如果是*add就不能用sizeof(addr)，可以写sizeof(struct sockaddr_in)
            if(ret < 0) //绑定失败
            {
                return-2;
            }

            ret = listen(tcp_sock_, 5); //backlog：指定内核中已完成队列的大小
            if(ret < 0) //监听失败
            {
                return -3;
            }
           
            std::string msg = "listen port is 17878";
            Log(INFO, __FILE__, __LINE__, "listen port is ") << tcp_port << std::endl;
            //LOG(INFO, "listen port is ") << tcp_port << std::endl;


            //创建用户管理模块的指针
            user_manager_ = new UserManager();
            if(!user_manager_)
            {
                Log(INFO, __FILE__, __LINE__, msg) << std::endl;
                return -1;
            }

            //创建消息池
            msg_pool_ = new MsgPool(1024);
            if(msg_pool_ == NULL)
            {
                LOG(ERROR, "init msgpool failed") << std::endl;
                return -1;
            }

            //考虑UDP通信
            //创建UDP套接字
            udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
            if(udp_port_ < 0)
            {
                LOG(ERROR, "create udp socket failed") << std::endl;
                return -1;
            }

            ret = bind(udp_sock_, (struct sockaddr*)&addr, sizeof(addr));
            if(ret < 0)
            {
                LOG(ERROR, "bind udp port failed") << std::endl;
                return -2;
            }

            LOG(INFO, "udp bind port is ") << UDP_PORT <<std::endl;
            LOG(INFO, "Server init success...") << std::endl;
        
            return 0; 
        }

        
        //启动线程 
        int Start()
        {
            //将UDP的线程放到前面，UDP线程创建完成之后，就去跑UDP的入口函数去了，就不用管了
            //TCP有个accept线程是阻塞的，阻塞掉之后就不能够创建UDP线程
    
            //udp应该有两类线程
            //   1类：生产线程，负责接收udp数据，将udp数据放到消息池当中
            //   2类：消费线程，负责从消息池当中获取消息，发送到在线用户的客户端
            //tcp
            //针对每一个注册登录请求(tcp连接)，针对每一个tcp连接，都创建一个线程，专门为该客户端处理注册和登录请求
            //注册和登录完毕之后，服务端关闭连接，销毁线程
        
            //tcp是否创建线程，取决于accept函数是否调用成功(阻塞)
            

            
            //UDP线程的创建
            for(int i = 0; i < THREAD_COUNT; i++)
            {
                //创建消费线程
                int ret = pthread_create(&con_tid_[i], NULL, ConsumeStart, (void*)this);//传入this指针
                if(ret < 0)
                {
                    LOG(ERROR, "Start udp thread failed") << std::endl;
                    return -1;
                }
                
                //创建生产线程
                ret = pthread_create(&pro_tid_[i], NULL, ProductStart, (void*)this);
                if(ret < 0)
                {
                    LOG(ERROR, "Start udp thread failed") << std::endl;
                    return -1;
                }
            }


            //tcp
            struct sockaddr_in peer_addr;
            socklen_t peer_addrlen = sizeof(peer_addr);
            while(1)
            {
                int new_sock = accept(tcp_sock_, (struct sockaddr*)&peer_addr, &peer_addrlen); //peer_addrlen需要取地址
                if(new_sock < 0) //不能因为一个接收失败而return退出掉，继续接收即可
                {
                    continue;
                }

                //正常接收到了
                //创建线程，为客户端的注册和登录请求负责
                
                TcpConnect* tc = new TcpConnect();
                tc->SetSockFd(new_sock); //new_sock传递进去
                tc->SetServer((void*)this); //this指针传递进去

                pthread_t tid; //线程标识符
                int ret = pthread_create(&tid, NULL, LoginRegisterStart, (void*)tc);
                //不能传递临时变量
                //需要将新连接的套接字new_sock传递给入口函数
                //LoginRegisterSrart表示线程的入口函数，线程执行代码的开始是从当前函数指针保存的函数开始运行
                //LoginRegisterStart是不会暴露给上层，不会有人调用，只是线程会用
                //最后一个参数表表示线程入口函数传递的参数，必须强转为(void*)
                if(ret < 0) //线程创建失败了
                {
                    close(new_sock); //关闭连接的套接字
                    delete tc; //释放堆上开辟的空间
                    continue;
                }

            }
        }


    private:
        //类中创建线程，类中定义的入口函数的时候，它会默认加上类的this指针。要加上static之后，这个函数为类服务，不为类的某个对象所服务。
        //static修饰的类成员属于类，不属于对象。static类成员函数是没有this指针的，this指针是指向本对象的指针
        //没有this指针，所以只能访问static类成员函数，不能访问非static的类成员，只能访问static修饰的类成员
        

        //生产线程：从网络当中接收udp数据，并且将udp数据放到消息池当中
        static void* ProductStart(void* arg)
        {
            pthread_detach(pthread_self()); //分离
            ChatServer* cs = (ChatServer*)arg; //拿到this指针
            //1.接收udp数据
            //2.将数据发送到消息池当中
            while(1) 
            {
                cs->RecvMsg();
            }
        }


        //消费线程：从消息池当中获取消息，并且将消息，广播给在线用户
        static void* ConsumeStart(void* arg)
        {
            pthread_detach(pthread_self()); //分离
            ChatServer* cs = (ChatServer*)arg; //拿到this指针
            //1.从消息池当中获取消息
            //2.推送给所有的在线用户
            while(1)
            {
                cs->SendMsg();
            }
        }


        static void* LoginRegisterStart(void* arg) 
        {
            //完成两件事：
            //注册
            //登录
            

            //若是注册，线程执行完就退出；若是登录，线程执行完也退出。
            //不会保持长连接，tcp只执行注册和登录
            //1.分离自己，当线程退出之后，线程所占用的资源就被操作系统回收了
            pthread_detach(pthread_self()); //设置分离属性

            //2.接收一个字节的数据，进而判断请求的类型，根据不同的请求类型，调用不同的函数进行处理(注册或者登录)
            TcpConnect* tc = (TcpConnect*)arg; //arg强转为tc  下面recv要用到new_sock,需要将新连接的套接字new_sock传递给入口函数
            ChatServer* cs = (ChatServer*)tc->GetServer();//arg强转为tc，tc的GetServer强转为cs就是this指针 

            char ques_type = -1;
            ssize_t recv_size = recv(tc->GetSockFd(), &ques_type, 1, 0);//recv：接收数据，tc->GetSockFd()拿到new_sock
            if(recv_size < 0)
            {
                //接收失败，就没必要再继续接收了
                //若要登录或注册，继续发送连接
                //关闭新连接套接字
                close(tc->GetSockFd());
                return NULL;//因为是线程入口函数(void *)，所以返回NULL，线程退出
            }
            else if(recv_size == 0) //表示对端关闭连接了
            {
                //关闭自己的连接
                close(tc->GetSockFd());
                return NULL; //return NULL也可以退出结束线程
            }

            //接收回来一个字节的数据
            //判断是注册请求还是登录请求
            int resp_status = -1;
            uint32_t user_id;
            switch(ques_type)
            {
                //看ConnectInfo中是怎么定义的
                case REGISTER_RESQ:
                    {
                        //处理注册请求,DealRegister 
                        resp_status = cs->DealRegister(tc->GetSockFd(), &user_id);
                        break;
                    }
                case LOGIN_RESQ:
                    {
                        //处理登录请求,DealLogin 
                        resp_status = cs->DealLogin(tc->GetSockFd(), &user_id);
                        break;
                    }
            }

            //响应的构造:注册或登录之后返回的状态以及ID
            //struct ReplyInfo
            //   status
            //   userid
            
            //服务端调用send接口给客户端发送注册之后的状态以及ID
            struct ReplyInfo ri;
            ri.resp_status_ = resp_status;
            ri.id_ = user_id;

            LOG(INFO, "resp_status is") << ri.resp_status_ << std::endl;
            LOG(INFO, "id is ") << ri.id_ << std::endl;
            
            int max_round_count = MAX_ROUND_COUNT;
            while(max_round_count > 0) //发送10次之后就退出
            //不能循环太多次，若有太多线程一直没有发送成功，线程一直占用，没释放
            {
                ssize_t send_size = send(tc->GetSockFd(), &ri, sizeof(ri), 0);
                if(send_size >= 0)
                {
                    LOG(WARNING, "Send reply success") << std::endl;
                    break;
                }

                //发送失败之后，是否需要重复发送？（需不需要将待发送的数据进行缓存下来）
                LOG(WARNING, "Send reply failed") << std::endl;
                max_round_count--;
            }

            //发送完毕之后关闭当前连接的套接字
            close(tc->GetSockFd());
            delete tc; //释放内存
            tc = NULL;
            return NULL;
        }


    private:
        //处理注册请求
        //不管是注册成功了，还是注册失败了，都需要给客户端返回一个应答
        int DealRegister(int new_sock, uint32_t* user_id) // DealRegisters是一个成员函数，默认传递this指针 
                                                       //给一个参数new_sock，因为tcp还要接收数据，需要用到new_sock
        {
            //继续从tcp连接当中接收注册数据，策略就是：直接使用RegisterInfo进行接收
            //客户注册的数据就保存在ri里面
            struct RegisterInfo ri;

            ssize_t recv_size = recv(new_sock, &ri, sizeof(ri), 0);
            if(recv_size < 0)
            {
                return -1;
            }
            else if(recv_size == 0)
            {
                close(new_sock);
                return -2;
            }

            //正常接收到了
            //需要将数据递交给用户管理模块，进行注册，并且将用户数据进行留存
            //需要和用户管理模块进行交互
            //调用UserManager类中的函数，要么有对象，要么有实例化指针
            int ret = user_manager_->DealRegister(ri.nick_name_, ri.school_, ri.passwd_, user_id);//将这些数据传到用户管理模块
            if(ret < 0)
            {
                //注册失败了
                return REGISTER_FAILED;
            }
            //注册成功了
            return REGISTER_SUCCESS;
        }


        //处理登录请求
        int DealLogin(int new_sock, uint32_t* user_id)
        {
            //继续从tcp连接当中接收登陆数据，策略就是：直接使用LoginInfo接收数据
            struct LoginInfo li;

            ssize_t recv_size = recv(new_sock, &li, sizeof(li), 0);
            if(recv_size < 0)
            {
                return -1;
            }
            else if(recv_size == 0)
            {
                close(new_sock);
                return -2;
            }

            *user_id = li.id_;//保存用户id

            //正常的逻辑，接收回来登录的请求
            //调用用户管理模块，判断登录请求当中的ID和密码是否正确
            int ret = user_manager_->DealLogin(li.id_, li.passwd_);
            if(ret < 0)
            {
                return LOGIN_FAILED;
            }
            return LOGIN_SUCCESS;
        }

        //生产线程调用这个函数,接收数据
        int RecvMsg()
        {
            //1.接收udp数据
            //2.判断该用户是否是登录用户(判断用户管理模块的状态即可)
            //3.判断该用户是否是第一次发送udp数据
            //     如果是：需要保存该用户的udp地址，并且将该用户放到在线用户列表当中
            //     如果不是：说明这个用户是老用户，之前已经保存过该用户的udp地址信息了
            //4.将数据发送到消息池当中
            char buf[UDP_MAX_DATA_LEN] = {0};
            struct sockaddr_in peer_addr;
            socklen_t peer_addr_len = sizeof(peer_addr);
            ssize_t recv_size = recvfrom(udp_sock_, buf, sizeof(buf) - 1, 0 , (struct sockaddr*)&peer_addr, &peer_addr_len);
            if(recv_size < 0)
            {
                perror("recvfrom");
                sleep(1);
                LOG(ERROR, "recv udp mag failed") << std::endl;
                return -1;
            }
             
            UdpMsg um; //定义一个对象(接收消息的数据格式)
            std::string msg;
            msg.assign(buf, strlen(buf));
            um.deserialize(msg); //反序列化是输入流

            //需要使用user_id和用户管理模块进行验证
            // 1.先使用该user_id去map当中查找
            // 2.需要判断当前用户的状态，保存用户udp地址信息
            if(user_manager_->IsLogin(um.user_id_, peer_addr, peer_addr_len) < 0)
            {
                return -1;
            }

            //正常逻辑
            msg_pool_->PushMsg(msg); //推送到消息池，不需要反序列化
            return 0;
        }


        //消费线程调用这个函数,发送消息
        int SendMsg()
        {
            //1.从消息池当中获取消息
            //2.按照在线用户列表推送消息
            std::string msg;
            msg_pool_->PopMsg(&msg);

            std::vector<UserInfo> vec;
            user_manager_->GetOnlineUser(&vec); //将在线用户保存在vec当中

            for(size_t i = 0; i < vec.size(); i++)
            {
                //调用原生的udp发送接口进行发送数据
                SendUdpMsg(msg, vec[i].GetAddrInfo(), vec[i].GetAddrLen());
                std::cout << i << ":" << msg << "==>" << inet_ntoa(vec[i].GetAddrInfo().sin_addr) << std::endl;
            }
            return 0;
        }
        
        int SendUdpMsg(std::string& msg, struct sockaddr_in addr, socklen_t addr_len) //发送的消息，发送的地址信息，地址信息长度
        {
            ssize_t send_size = sendto(udp_sock_, msg.c_str(), msg.size(), 0, (struct sockaddr*)&addr, addr_len);
            if(send_size < 0)
            {
                //本质上如果推送失败了，要想方设法的再次给客户端推送
                //要么缓存消息，要么在第一次发送的时候，多尝试几次
                LOG(ERROR, "send msg failed, msg is") << msg << "ip is" << inet_ntoa(addr.sin_addr) << "port is" << ntohs(addr.sin_port) << std::endl;
                return -1;
            }

            return 0;
        }

    private:
        //1.考虑有什么成员变量：TCP套接字、UDP套接字
        //2.再考虑成员函数，还会用到哪些成员变量
        

        //客户端与服务端能够发消息的第一个步骤就是注册与登录，注册和登录用到TCP
        int tcp_sock_;
        uint16_t tcp_port_;

        int udp_sock_;
        uint16_t udp_port_; 

        UserManager* user_manager_; //用户管理模块的实例化指针

        //UDP线程的标识符数组
        pthread_t con_tid_[THREAD_COUNT];
        pthread_t pro_tid_[THREAD_COUNT];

        MsgPool* msg_pool_; //消息池的实例化指针

        UdpMsg* udp_msg; //Udp接收消息的实例化指针
};
